Skip to content

Conversation

@jackd248
Copy link
Owner

@jackd248 jackd248 commented Oct 13, 2025

Summary by CodeRabbit

  • New Features

    • Automatic extraction of authors and license from composer.json for docblock headers.
    • Added serialization of generated headers to array.
    • Added configuration preset support for simpler setup.
  • Documentation

    • Added example showing auto-read of authors and license from composer.json.
  • Tests

    • Comprehensive tests for composer.json parsing, header generation, edge cases, and PHP 8.2+ scenarios.
    • Expanded coverage for class-name handling and anonymous classes.
  • Style

    • Standardized file headers and streamlined imports.
  • Chores

    • Updated dev dependencies and autoload-dev mappings.

@coderabbitai
Copy link

coderabbitai bot commented Oct 13, 2025

Walkthrough

Swaps PHP-CS-Fixer preset to konradmichalik/php-cs-fixer-preset, adds composer-driven DocBlock header generation, introduces ComposerService, extends DocBlockHeader with fromComposer and __toArray, normalizes token constants in the fixer, updates composer.json/README, and expands tests and headers/licenses across files.

Changes

Cohort / File(s) Summary of changes
CS Fixer config migration
\.php-cs-fixer.php
Replaced EliasHaeussler config with KonradMichalik preset; registers DocBlockHeaderFixer; uses Header::fromComposer() and Set::fromArray(DocBlockHeader::fromComposer()->__toArray()); streamlined Finder; removed legacy config.
Composer + README
composer.json, README.md
Removed eliashaeussler/php-cs-fixer-config; added konradmichalik/php-cs-fixer-preset; added autoload-dev PSR-4 mapping for tests; appended README example showing DocBlockHeader::fromComposer() + registerCustomFixers().
Composer metadata service
src/Service/ComposerService.php
New ComposerService with readComposerJson, extractLicense, extractAuthors, getPrimaryAuthor including validation and RuntimeException error paths.
Generator enhancements
src/Generators/DocBlockHeader.php, src/Generators/Generator.php
Added DocBlockHeader::fromComposer() and __toArray(); changed default addStructureName in create() to true; updated imports/docs; added PHPDoc to Generator interface.
Fixer internals (tokens)
src/Rules/DocBlockHeaderFixer.php
Normalized token checks and Token construction to fully-qualified constants (\T_*), grouped imports, added function imports; behavior preserved.
Enum & misc headers
src/Enum/Separate.php, rector.php
Rewrote file headers/docblocks and removed GPL boilerplate in favor of LICENSE references; no API or behavioral changes.
Tests: Generators
tests/src/Generators/DocBlockHeaderTest.php
Added tests for fromComposer (single/multiple/no authors, no license, extra annotations, custom params); updated expectations for add_structure_name; new toArray assertions.
Tests: Fixer
tests/src/Rules/DocBlockHeaderFixerTest.php
Added testGetPriority(), expanded coverage for readonly/anonymous classes and structure-name behavior; adapted tests to \T_* constants.
Tests: Service
tests/src/Service/ComposerServiceTest.php
New comprehensive tests covering successful read, error modes (missing/unreadable/invalid JSON), license/author extraction and primary author retrieval.
Tests: Enum
tests/src/Enum/SeparateTest.php
Header/docblock updates only; no functional changes.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Dev as Developer
  participant CS as php-cs-fixer
  participant Preset as Preset (Config/Set)
  participant Fixer as DocBlockHeaderFixer
  participant Gen as DocBlockHeader (fromComposer)
  participant Svc as ComposerService
  participant FS as Filesystem

  Dev->>CS: run php-cs-fixer
  CS->>Preset: load preset (Header::fromComposer, Set::fromArray(...))
  CS->>Fixer: apply custom fixer
  Fixer->>Gen: request header config (__toArray)
  Gen->>Svc: fromComposer(composer.json)
  Svc->>FS: read composer.json
  FS-->>Svc: JSON contents
  Svc-->>Gen: authors/license data
  Gen-->>Fixer: annotations array
  Fixer->>CS: insert/merge DocBlocks
  CS-->>Dev: files fixed
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

I nibbled through configs at dawn's gentle shim,
Swapped presets and headers with a hop and a whim.
Composer now hums authors and license by name,
My fixer stitches docblocks and tidies the frame.
Thump-thump—tests pass, carrots for tea, code tame. 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title directly references the introduction of the ComposerService class, which is a key new component within this pull request, and thus it accurately reflects a real part of the changeset.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch from-composer

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 063b6dc and d8a5356.

📒 Files selected for processing (1)
  • tests/src/Rules/DocBlockHeaderFixerTest.php (10 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
tests/src/Rules/DocBlockHeaderFixerTest.php (1)
src/Rules/DocBlockHeaderFixer.php (2)
  • getPriority (55-60)
  • configure (96-99)
🔇 Additional comments (3)
tests/src/Rules/DocBlockHeaderFixerTest.php (3)

53-56: LGTM!

The test correctly verifies that getPriority() returns 1, which aligns with the implementation where the fixer runs before other rules with priority 0.


555-555: LGTM!

The update to use fully-qualified token constants (e.g., \T_CLASS instead of T_CLASS) is correct and improves code clarity.


955-976: Ignore parse-error warning; tests are safe. The code wraps <?php $obj = new readonly class {}; in a single-quoted string, so the readonly keyword isn’t parsed at compile time. “Readonly classes” were introduced in PHP 8.3, so #[RequiresPhp('>= 8.3')] is correct.

Likely an incorrect or invalid review comment.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (8)
tests/src/Enum/SeparateTest.php (1)

24-29: Consider consolidating repetitive enum assertions with data providers

Optional: reduce duplication via PHPUnit attributes with a data provider for from/tryFrom/value assertions.

Based on learnings

Example:

use PHPUnit\Framework\Attributes\DataProvider;

#[DataProvider('casesProvider')]
public function testFromStringProvider(string $in, Separate $expected): void
{
    self::assertSame($expected, Separate::from($in));
}

public static function casesProvider(): array
{
    return [
        ['top', Separate::Top],
        ['bottom', Separate::Bottom],
        ['both', Separate::Both],
        ['none', Separate::None],
    ];
}
src/Generators/Generator.php (1)

16-21: Minor naming nit: avoid “__” prefix for non-magic methods

Optional: consider renaming __toArray() to toArray() to prevent confusion with PHP magic methods.

For clarity, the interface could be:

interface Generator
{
    /** @return array<string, array<string, mixed>> */
    public function toArray(): array;
}
README.md (1)

113-127: Clarify defaults and failure modes for fromComposer()

  • fromComposer() will throw if composer.json is missing/invalid. Consider a short note or path override hint.
  • Also, generator default add_structure_name is true, while the fixer option’s documented default is false below. Consider clarifying that the generator sets it explicitly (so users aren’t surprised).
src/Rules/DocBlockHeaderFixer.php (1)

339-346: Support hyphenated tags when parsing existing annotations

parseExistingAnnotations ignores tags like property-read and used-by. Widen the pattern.

Apply this diff:

-            if (preg_match('/^@(\w+)(?:\s+(.*))?$/', $line, $matches)) {
+            if (preg_match('/^@([A-Za-z][A-Za-z0-9_-]*)(?:\s+(.*))?$/', $line, $matches)) {
src/Service/ComposerService.php (1)

49-56: Use JSON_THROW_ON_ERROR for clearer failures

Improves error messages and avoids silent nulls from json_decode.

Apply this diff:

-        $data = json_decode($contents, true);
-
-        if (!is_array($data)) {
-            throw new RuntimeException(sprintf('Unable to decode JSON from file "%s".', $composerJsonPath));
-        }
+        try {
+            $data = json_decode($contents, true, 512, \JSON_THROW_ON_ERROR);
+        } catch (\JsonException $e) {
+            throw new RuntimeException(sprintf('Unable to decode JSON from file "%s": %s', $composerJsonPath, $e->getMessage()), 0, $e);
+        }
+        if (!is_array($data)) {
+            throw new RuntimeException(sprintf('Unexpected JSON root type in "%s": %s', $composerJsonPath, gettype($data)));
+        }
tests/src/Service/ComposerServiceTest.php (3)

81-83: Nit: correct comment (warning, not notice).

-            // Suppress the expected PHP notice when reading a directory
+            // Suppress the expected PHP warning when reading a directory

20-29: Merge duplicated docblocks; keep a single class docblock after the attribute.

The standalone “@internal” docblock is detached and not applied to the class. Merge it into the main docblock.

-/**
- * @internal
- */
-#[\PHPUnit\Framework\Attributes\CoversClass(ComposerService::class)]
-/**
- * ComposerServiceTest.
- *
- * @author Konrad Michalik <hej@konradmichalik.dev>
- * @license GPL-3.0-or-later
- */
+#[\PHPUnit\Framework\Attributes\CoversClass(ComposerService::class)]
+/**
+ * ComposerServiceTest.
+ *
+ * @internal
+ * @author Konrad Michalik <hej@konradmichalik.dev>
+ * @license GPL-3.0-or-later
+ */

36-37: Prefer tempnam() for safer temp filenames.

-        $this->testComposerJsonPath = sys_get_temp_dir() . '/test-composer-' . uniqid() . '.json';
+        $tmp = tempnam(sys_get_temp_dir(), 'composer-');
+        self::assertNotFalse($tmp);
+        $this->testComposerJsonPath = $tmp;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d0d2e02 and 7dc8a2c.

⛔ Files ignored due to path filters (1)
  • composer.lock is excluded by !**/*.lock
📒 Files selected for processing (13)
  • .php-cs-fixer.php (1 hunks)
  • README.md (1 hunks)
  • composer.json (2 hunks)
  • rector.php (1 hunks)
  • src/Enum/Separate.php (1 hunks)
  • src/Generators/DocBlockHeader.php (2 hunks)
  • src/Generators/Generator.php (1 hunks)
  • src/Rules/DocBlockHeaderFixer.php (12 hunks)
  • src/Service/ComposerService.php (1 hunks)
  • tests/src/Enum/SeparateTest.php (2 hunks)
  • tests/src/Generators/DocBlockHeaderTest.php (4 hunks)
  • tests/src/Rules/DocBlockHeaderFixerTest.php (10 hunks)
  • tests/src/Service/ComposerServiceTest.php (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
src/Generators/DocBlockHeader.php (2)
src/Service/ComposerService.php (4)
  • ComposerService (30-107)
  • readComposerJson (37-56)
  • extractAuthors (77-94)
  • extractLicense (61-70)
src/Generators/Generator.php (1)
  • __toArray (27-27)
tests/src/Generators/DocBlockHeaderTest.php (1)
src/Generators/DocBlockHeader.php (2)
  • DocBlockHeader (32-150)
  • fromComposer (74-115)
tests/src/Rules/DocBlockHeaderFixerTest.php (1)
src/Rules/DocBlockHeaderFixer.php (2)
  • getPriority (55-60)
  • configure (96-99)
tests/src/Service/ComposerServiceTest.php (1)
src/Service/ComposerService.php (5)
  • ComposerService (30-107)
  • readComposerJson (37-56)
  • extractLicense (61-70)
  • extractAuthors (77-94)
  • getPrimaryAuthor (101-106)
.php-cs-fixer.php (3)
src/Generators/DocBlockHeader.php (4)
  • DocBlockHeader (32-150)
  • create (60-69)
  • fromComposer (74-115)
  • __toArray (45-55)
src/Rules/DocBlockHeaderFixer.php (1)
  • DocBlockHeaderFixer (35-420)
src/Generators/Generator.php (1)
  • __toArray (27-27)
🪛 GitHub Actions: Tests
tests/src/Rules/DocBlockHeaderFixerTest.php

[error] 958-958: ParseError: syntax error, unexpected token "readonly" during test execution in anonymous class. Occurred while running 'composer test' (PHPUnit).


[error] 980-980: ParseError: syntax error, unexpected token "readonly" during test execution in anonymous class. Occurred while running 'composer test' (PHPUnit).

🪛 GitHub Check: cgl / cgl
tests/src/Service/ComposerServiceTest.php

[warning] 1-1:
Found violation(s) of type: concat_space

🔇 Additional comments (13)
tests/src/Enum/SeparateTest.php (1)

6-11: Header/license standardization LGTM

Consistent with the repo-wide header refactor.

src/Enum/Separate.php (1)

6-11: Docs/header updates LGTM

Enum doc and header align with new policy; no behavior change.

Also applies to: 16-21

src/Generators/Generator.php (1)

6-11: Header/license standardization LGTM

Matches project-wide convention.

rector.php (1)

6-11: Header/license standardization LGTM

No functional changes; config remains intact.

composer.json (2)

43-47: PSR-4 autoload-dev mapping LGTM

Namespace matches tests/src; remember to run composer dump-autoload.


32-32: Run Composer compatibility checks
Ensure konradmichalik/php-cs-fixer-preset:^0.1.0 and friendsofphp/php-cs-fixer:^3.14 don’t conflict with your PHP matrix. Locally execute:

composer why-not konradmichalik/php-cs-fixer-preset ^0.1.0
composer why-not friendsofphp/php-cs-fixer ^3.14
composer prohibits konradmichalik/php-cs-fixer-preset:^0.1.0 friendsofphp/php-cs-fixer:^3.14
composer update --dry-run konradmichalik/php-cs-fixer-preset friendsofphp/php-cs-fixer
tests/src/Generators/DocBlockHeaderTest.php (1)

308-482: Solid coverage for fromComposer scenarios

Covers single/multiple/no author, missing license, extra annotations, and custom flags. Looks good.

src/Rules/DocBlockHeaderFixer.php (1)

111-117: Token constant refactor LGTM

Using fully-qualified token constants improves clarity and avoids import ambiguity.

.php-cs-fixer.php (1)

14-34: Verify preset dependency and rule compatibility

Ensure konradmichalik/php-cs-fixer-preset is present and Set::fromArray(...) accepts the generator’s array as intended. Also confirm CI uses PHP ≥ 8.1 (enums, token constants).

tests/src/Rules/DocBlockHeaderFixerTest.php (1)

53-56: Priority assertion matches implementation

Good to lock getPriority() to 1.

src/Generators/DocBlockHeader.php (2)

60-69: Default addStructureName=true – confirm intent

create() now defaults addStructureName to true; tests and README examples align, but the fixer’s option default remains false. Confirm this divergence is intentional to keep OO/generator path verbose by default.


74-115: fromComposer() implementation LGTM

Correctly builds author(s) and license, merges additional annotations post-compute, and delegates to create().

tests/src/Service/ComposerServiceTest.php (1)

1-1: Style: concat_space violation flagged by CGL.

Static analysis reported concat_space. Please run your fixer (php-cs-fixer/cgl) and align concatenation spacing with the repo’s rule (one or none).

Comment on lines +221 to 229
if ($token->isGivenKind(\T_DOC_COMMENT)) {
return $i;
}

// If we hit any other meaningful token (except modifiers), stop looking
if (!$token->isGivenKind([T_FINAL, T_ABSTRACT, T_ATTRIBUTE])) {
if (!$token->isGivenKind([\T_FINAL, \T_ABSTRACT, \T_ATTRIBUTE])) {
break;
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Treat T_READONLY as a class modifier in lookback/insert logic

Readonly classes (PHP 8.2+) can have a T_READONLY before T_CLASS. Not handling it may miss existing DocBlocks and insert in the wrong spot.

Apply this diff:

-            if (!$token->isGivenKind([\T_FINAL, \T_ABSTRACT, \T_ATTRIBUTE])) {
+            if (!$token->isGivenKind([\T_FINAL, \T_ABSTRACT, \T_ATTRIBUTE, \T_READONLY])) {
                 break;
             }
-            if ($token->isGivenKind([\T_FINAL, \T_ABSTRACT, \T_ATTRIBUTE])) {
+            if ($token->isGivenKind([\T_FINAL, \T_ABSTRACT, \T_ATTRIBUTE, \T_READONLY])) {
                 $insertIndex = $i;
                 continue;
             }

Also applies to: 318-321

🤖 Prompt for AI Agents
In src/Rules/DocBlockHeaderFixer.php around lines 221-229 (and also update the
similar check at lines 318-321), the lookback/insert logic treats class
modifiers but currently omits T_READONLY; add \T_READONLY to the array of
modifier token kinds so readonly classes are recognized as modifiers and the
DocBlock lookup/insert point is correct. Make the exact change in both locations
by including \T_READONLY in the list passed to isGivenKind.

Comment on lines +955 to 975
public function testIsAnonymousClassWithReadonlyModifier(): void
{
$code = '<?php $obj = new readonly class {};';
$tokens = Tokens::fromCode($code);

$method = new ReflectionMethod($this->fixer, 'isAnonymousClass');

// Find the class token index
$classIndex = null;
for ($i = 0; $i < $tokens->count(); ++$i) {
if ($tokens[$i]->isGivenKind(\T_CLASS)) {
$classIndex = $i;
break;
}
}

$result = $method->invoke($this->fixer, $tokens, $classIndex);

// This tests line 156: continue when token is T_READONLY or T_FINAL
self::assertTrue($result);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix CI parse errors: gate “readonly class” tests to PHP ≥ 8.2

On PHP < 8.2, tokenizing '<?php $obj = new readonly class {};' fails. Gate these tests to avoid ParseError.

Apply this diff:

     public function testIsAnonymousClassWithReadonlyModifier(): void
     {
+        if (\PHP_VERSION_ID < 80200) {
+            self::markTestSkipped('Requires PHP 8.2+ (readonly classes).');
+        }
         $code = '<?php $obj = new readonly class {};';
         $tokens = Tokens::fromCode($code);
         $method = new ReflectionMethod($this->fixer, 'isAnonymousClass');
         // Find the class token index
         $classIndex = null;
         for ($i = 0; $i < $tokens->count(); ++$i) {
             if ($tokens[$i]->isGivenKind(\T_CLASS)) {
                 $classIndex = $i;
                 break;
             }
         }
         $result = $method->invoke($this->fixer, $tokens, $classIndex);
         // This tests line 156: continue when token is T_READONLY or T_FINAL
         self::assertTrue($result);
     }
 
     public function testSkipsAnonymousClassWithReadonlyModifier(): void
     {
+        if (\PHP_VERSION_ID < 80200) {
+            self::markTestSkipped('Requires PHP 8.2+ (readonly classes).');
+        }
         $code = '<?php $obj = new readonly class {};';
         $tokens = Tokens::fromCode($code);
         $file = new SplFileInfo(__FILE__);
         $method = new ReflectionMethod($this->fixer, 'applyFix');
         $this->fixer->configure([
             'annotations' => ['author' => 'John Doe'],
             'separate' => 'none',
             'ensure_spacing' => false,
         ]);
         $method->invoke($this->fixer, $file, $tokens);
         // Anonymous class with readonly modifier should NOT have DocBlock added
         self::assertSame($code, $tokens->generateCode());
     }

Alternatively, use #[\PHPUnit\Framework\Attributes\RequiresPhp('>= 8.2')]. Based on learnings

Also applies to: 977-994

🧰 Tools
🪛 GitHub Actions: Tests

[error] 958-958: ParseError: syntax error, unexpected token "readonly" during test execution in anonymous class. Occurred while running 'composer test' (PHPUnit).

🤖 Prompt for AI Agents
In tests/src/Rules/DocBlockHeaderFixerTest.php around lines 955-975 (and
similarly 977-994), the test that tokenizes '<?php $obj = new readonly class
{};' fails on PHP < 8.2; update the test to gate it to PHP >= 8.2 by adding a
PHPUnit requirement or runtime skip: either annotate the test with
#[\PHPUnit\Framework\Attributes\RequiresPhp('>= 8.2')] or add an early runtime
check that calls $this->markTestSkipped('requires PHP >= 8.2') when
PHP_VERSION_ID < 80200, so the test only runs on PHP 8.2+ and avoids ParseError
on older interpreters.

Comment on lines +100 to +103
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Unable to read file');
// Suppress the expected PHP warning when reading an unreadable file
@ComposerService::readComposerJson($invalidPath);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix fragile exception assertion (exact message mismatch).

Exception message includes the file path; use a regex match.

-                $this->expectException(RuntimeException::class);
-                $this->expectExceptionMessage('Unable to read file');
+                $this->expectException(RuntimeException::class);
+                $this->expectExceptionMessageMatches('/Unable to read file/');
🤖 Prompt for AI Agents
In tests/src/Service/ComposerServiceTest.php around lines 100 to 103, the test
currently asserts an exact exception message but the actual message includes the
file path so the assert is brittle; replace the exact-message assertion with a
regex-based assertion (use PHPUnit's expectExceptionMessageMatches and construct
a pattern that matches "Unable to read file" and the optional path — build the
pattern using preg_quote($invalidPath, '/') so the path is escaped) and keep the
rest of the test (including the warning suppression) unchanged.

Comment on lines +119 to +123
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Unable to decode JSON');

ComposerService::readComposerJson($this->testComposerJsonPath);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix invalid JSON assertion to allow dynamic file path.

Match the invariant part of the message.

-        $this->expectException(RuntimeException::class);
-        $this->expectExceptionMessage('Unable to decode JSON');
+        $this->expectException(RuntimeException::class);
+        $this->expectExceptionMessageMatches('/Unable to decode JSON/');
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Unable to decode JSON');
ComposerService::readComposerJson($this->testComposerJsonPath);
}
$this->expectException(RuntimeException::class);
$this->expectExceptionMessageMatches('/Unable to decode JSON/');
ComposerService::readComposerJson($this->testComposerJsonPath);
}
🤖 Prompt for AI Agents
In tests/src/Service/ComposerServiceTest.php around lines 119-123, the test
currently asserts the full exception message which includes a dynamic file path;
replace the strict equality assertion with a pattern match so only the invariant
part is checked. Specifically, change the expectExceptionMessage(...) call to
use expectExceptionMessageMatches('/Unable to decode JSON/') (or an equivalent
regex-based expectation) so the test accepts messages containing that phrase
plus any dynamic path.

@coveralls
Copy link

coveralls commented Oct 13, 2025

Pull Request Test Coverage Report for Build 18461941382

Details

  • 80 of 80 (100.0%) changed or added relevant lines in 3 files are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage increased (+1.4%) to 100.0%

Totals Coverage Status
Change from base Build 18461729480: 1.4%
Covered Lines: 260
Relevant Lines: 260

💛 - Coveralls

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (5)
src/Rules/DocBlockHeaderFixer.php (2)

24-25: Consider removing redundant function imports.

Importing in_array and is_array is valid but unnecessary, as these functions are already available globally in PHP. This adds no functional benefit and is uncommon in PHP codebases.

If you prefer this style for consistency or IDE support, it's acceptable to keep. Otherwise, consider removing:

-use function in_array;
-use function is_array;
-

64-67: Consider reverting to standard token constant syntax.

Throughout this file, token constants have been changed from T_CLASS to \T_CLASS (and similarly for other constants). While technically correct, this is inconsistent with standard PHP practices. Token constants are already in the global namespace and the backslash prefix is redundant.

Unless there's a specific reason for this change (e.g., compatibility with a static analysis tool or coding standard), consider reverting to the standard syntax:

-if ($token->isGivenKind(\T_CLASS)) {
+if ($token->isGivenKind(T_CLASS)) {

Apply this pattern throughout the file for all token constants.

Also applies to: 111-111, 116-116, 144-144, 155-155, 160-160, 201-201, 221-221, 226-226, 244-244, 259-259, 280-280, 285-285, 291-291, 299-299, 318-318, 416-416

tests/src/Service/ComposerServiceTest.php (1)

71-86: Consider the test behavior for reading a directory.

The test uses a directory to simulate an unreadable file, but the behavior may vary by platform:

  • On some systems, file_get_contents() on a directory might return a warning and false
  • On others, it might succeed with empty or garbage content that fails JSON decode

The regex pattern accounts for this variation, but the test name suggests it's testing "CannotBeRead" when it might actually be testing decode failure.

Consider either:

  1. Renaming to better reflect the actual test scenario (e.g., testReadComposerJsonThrowsExceptionOnInvalidInput)
  2. Or documenting the platform-specific behavior in a comment
src/Generators/DocBlockHeader.php (1)

74-115: Clarify annotation override behavior.

The implementation correctly extracts composer.json metadata and formats it into annotations. However, line 112's merge strategy means $additionalAnnotations can override extracted author/license values.

Consider documenting this behavior in the method's PHPDoc:

 /**
  * @param array<string, string|array<string>> $additionalAnnotations
+ *
+ * Note: $additionalAnnotations will override any annotations extracted from composer.json
+ * if they have the same key (e.g., 'author', 'license').
  */
 public static function fromComposer(

This makes the override behavior explicit for users who might want to supplement rather than replace certain annotations.

src/Service/ComposerService.php (1)

37-56: Consider more specific JSON error handling.

The implementation correctly validates file existence, read success, and JSON decoding. However, you could provide more specific error messages by checking json_last_error().

 $data = json_decode($contents, true);

-if (!is_array($data)) {
-    throw new RuntimeException(sprintf('Unable to decode JSON from file "%s".', $composerJsonPath));
+if (JSON_ERROR_NONE !== json_last_error()) {
+    throw new RuntimeException(sprintf(
+        'Unable to decode JSON from file "%s": %s',
+        $composerJsonPath,
+        json_last_error_msg()
+    ));
+}
+
+if (!is_array($data)) {
+    throw new RuntimeException(sprintf('Invalid JSON structure in file "%s": expected object, got %s.', $composerJsonPath, gettype($data)));
 }

This provides more actionable error messages for debugging, though the current implementation is acceptable for the use case.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 585a105 and 063b6dc.

⛔ Files ignored due to path filters (1)
  • composer.lock is excluded by !**/*.lock
📒 Files selected for processing (13)
  • .php-cs-fixer.php (1 hunks)
  • README.md (1 hunks)
  • composer.json (2 hunks)
  • rector.php (1 hunks)
  • src/Enum/Separate.php (1 hunks)
  • src/Generators/DocBlockHeader.php (2 hunks)
  • src/Generators/Generator.php (1 hunks)
  • src/Rules/DocBlockHeaderFixer.php (12 hunks)
  • src/Service/ComposerService.php (1 hunks)
  • tests/src/Enum/SeparateTest.php (2 hunks)
  • tests/src/Generators/DocBlockHeaderTest.php (4 hunks)
  • tests/src/Rules/DocBlockHeaderFixerTest.php (10 hunks)
  • tests/src/Service/ComposerServiceTest.php (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
tests/src/Generators/DocBlockHeaderTest.php (2)
src/Generators/DocBlockHeader.php (2)
  • DocBlockHeader (32-150)
  • fromComposer (74-115)
tests/src/Service/ComposerServiceTest.php (1)
  • PHPUnit (23-259)
src/Generators/DocBlockHeader.php (2)
src/Service/ComposerService.php (4)
  • ComposerService (30-107)
  • readComposerJson (37-56)
  • extractAuthors (77-94)
  • extractLicense (61-70)
src/Generators/Generator.php (1)
  • __toArray (27-27)
tests/src/Service/ComposerServiceTest.php (1)
src/Service/ComposerService.php (5)
  • ComposerService (30-107)
  • readComposerJson (37-56)
  • extractLicense (61-70)
  • extractAuthors (77-94)
  • getPrimaryAuthor (101-106)
tests/src/Rules/DocBlockHeaderFixerTest.php (1)
src/Rules/DocBlockHeaderFixer.php (2)
  • getPriority (55-60)
  • configure (96-99)
.php-cs-fixer.php (3)
src/Generators/DocBlockHeader.php (4)
  • DocBlockHeader (32-150)
  • create (60-69)
  • fromComposer (74-115)
  • __toArray (45-55)
src/Rules/DocBlockHeaderFixer.php (1)
  • DocBlockHeaderFixer (35-420)
src/Generators/Generator.php (1)
  • __toArray (27-27)
🪛 GitHub Actions: Tests
tests/src/Rules/DocBlockHeaderFixerTest.php

[error] 959-959: ParseError: syntax error, unexpected token "readonly"


[error] 982-982: ParseError: syntax error, unexpected token "readonly"

🔇 Additional comments (24)
src/Enum/Separate.php (1)

5-21: LGTM! Header standardization applied correctly.

The updated file header and class-level docblock align with the new standardized format being applied across the codebase. No functional changes to the enum itself.

tests/src/Enum/SeparateTest.php (1)

5-29: LGTM! Test file header standardization applied correctly.

The header and class-level docblock updates are consistent with the standardization being applied across the codebase.

README.md (1)

113-127: LGTM! Clear documentation of the new fromComposer feature.

The usage example effectively demonstrates how to automatically extract author and license information from composer.json. This is a valuable addition that shows the simplest configuration pattern.

src/Generators/Generator.php (1)

5-21: LGTM! Interface documentation standardized.

The header and interface-level docblock updates are consistent with the standardization being applied across the codebase. No changes to the public interface signature.

rector.php (1)

5-12: LGTM! Configuration file header standardized.

The header update is consistent with the standardization being applied across the codebase.

composer.json (2)

43-47: LGTM! Autoload-dev mapping added correctly.

The PSR-4 autoload-dev mapping for the test namespace is correctly configured and enables proper test discovery for the new test files.


32-32: composer.json: konradmichalik/php-cs-fixer-preset is available on Packagist (v0.1.0) and the autoload-dev mapping is correct.

tests/src/Generators/DocBlockHeaderTest.php (4)

18-18: LGTM! Import statement modernized with group syntax.

The grouped import for DocBlockHeader and Generator is a cleaner approach and aligns with modern PHP style.


26-31: LGTM! Test class documentation added.

The class-level docblock is consistent with the standardization being applied across the codebase.


307-483: LGTM! Comprehensive test coverage for fromComposer functionality.

The new test methods provide excellent coverage of the fromComposer feature:

  • Single and multiple authors with/without emails
  • Missing authors or license handling
  • Additional annotations merging
  • Custom parameter configurations

All tests properly create temporary files and clean them up in finally blocks, following best practices.


89-89: Tests explicitly configure add_structure_name; no default changed. The generator’s create() and fromComposer() methods have always defaulted to true, and the fixer option default remains false as documented. The tests aren’t relying on a new default but pass the flag explicitly, so there is no breaking change.

Likely an incorrect or invalid review comment.

src/Rules/DocBlockHeaderFixer.php (3)

5-12: LGTM! Header standardized.

The header update is consistent with the standardization being applied across the codebase.


19-21: LGTM! Grouped imports improve readability.

Consolidating the PhpCsFixer imports using grouped syntax makes the import section more compact and organized.


28-31: LGTM! Class-level documentation added.

The docblock is consistent with the standardization being applied across the codebase.

.php-cs-fixer.php (2)

21-34: LGTM!

The configuration structure is well-organized:

  • Custom fixer registration is properly implemented
  • The use of fromComposer() aligns with the PR's objective to enable composer.json-driven header generation
  • The __toArray() method correctly serializes the configuration for the preset system
  • Finder setup is appropriate

14-19: Dependency availability confirmed

Confirmed konradmichalik/php-cs-fixer-preset exists on Packagist at version ^0.1.0 (published 2025-10-08), matching composer.json.

tests/src/Service/ComposerServiceTest.php (3)

34-44: LGTM!

The setup and teardown methods properly ensure test isolation by creating unique temporary files and cleaning up after each test.


125-232: LGTM!

Comprehensive test coverage for license and author extraction:

  • Handles all expected data shapes (string, array, missing)
  • Properly filters invalid entries
  • Uses appropriate PHPStan suppressions for optional fields in structured arrays

234-258: LGTM!

The tests properly verify that getPrimaryAuthor returns the first author when multiple exist and null when none are present.

tests/src/Rules/DocBlockHeaderFixerTest.php (1)

53-56: LGTM!

The test properly verifies that getPriority() returns the expected value of 1, ensuring the fixer runs before other relevant fixers.

src/Generators/DocBlockHeader.php (1)

42-55: LGTM!

The __toArray() method correctly serializes the DocBlockHeader configuration in a format compatible with the PHP-CS-Fixer preset system, matching the fixer name used in DocBlockHeaderFixer.

src/Service/ComposerService.php (3)

61-70: LGTM!

The method correctly handles the Composer schema for licenses:

  • Returns null when license is not specified
  • Handles single license string
  • Returns the first license from an array
  • Gracefully handles empty arrays

77-94: LGTM!

The method properly extracts and validates authors according to the Composer schema:

  • Returns empty array for missing or invalid authors field
  • Filters out invalid entries (non-arrays or missing name)
  • Preserves optional fields (email, role)
  • Returns a properly typed list

101-106: LGTM!

The method correctly delegates to extractAuthors() and returns the first author or null. The implementation is clean and reuses existing validation logic.

Comment on lines 60 to 69
public static function create(
array $annotations,
bool $preserveExisting = true,
Separate $separate = Separate::Both,
bool $addStructureName = false,
bool $addStructureName = true,
): self {
self::validateAnnotations($annotations);

return new self($annotations, $preserveExisting, $separate, $addStructureName);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Note: Breaking change in default behavior.

The default value of $addStructureName has changed from false to true. This aligns the create() method with fromComposer() for consistency, but it changes the default behavior for existing code that relies on the previous default.

Consider:

  1. Documenting this breaking change in the release notes or changelog
  2. Evaluating whether this should be a major version bump according to semantic versioning

The change improves API consistency, but users who called create() without explicitly setting addStructureName will now get different output.

🤖 Prompt for AI Agents
In src/Generators/DocBlockHeader.php around lines 60 to 69 the default for
$addStructureName was changed to true which is a breaking behavioral change;
either revert the default back to false to preserve existing behavior or, if you
intentionally want the new default, document the change in the release
notes/changelog and increment the major version per semver; update unit tests
and any examples to reflect the chosen default and add a short changelog entry
noting the change in create() default for $addStructureName.

Comment on lines 955 to 996
#[\PHPUnit\Framework\Attributes\RequiresPhp('>= 8.2')]
public function testIsAnonymousClassWithReadonlyModifier(): void
{
$code = '<?php $obj = new readonly class {};';
$tokens = Tokens::fromCode($code);

$method = new ReflectionMethod($this->fixer, 'isAnonymousClass');

// Find the class token index
$classIndex = null;
for ($i = 0; $i < $tokens->count(); ++$i) {
if ($tokens[$i]->isGivenKind(\T_CLASS)) {
$classIndex = $i;
break;
}
}

$result = $method->invoke($this->fixer, $tokens, $classIndex);

// This tests line 156: continue when token is T_READONLY or T_FINAL
self::assertTrue($result);
}

#[\PHPUnit\Framework\Attributes\RequiresPhp('>= 8.2')]
public function testSkipsAnonymousClassWithReadonlyModifier(): void
{
$code = '<?php $obj = new readonly class {};';
$tokens = Tokens::fromCode($code);
$file = new SplFileInfo(__FILE__);

$method = new ReflectionMethod($this->fixer, 'applyFix');

$this->fixer->configure([
'annotations' => ['author' => 'John Doe'],
'separate' => 'none',
'ensure_spacing' => false,
]);
$method->invoke($this->fixer, $file, $tokens);

// Anonymous class with readonly modifier should NOT have DocBlock added
self::assertSame($code, $tokens->generateCode());
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix parse errors on PHP < 8.2.

The pipeline failures indicate that the readonly keyword in the test code causes parse errors on PHP versions below 8.2, even though the tests have RequiresPhp('>= 8.2') attributes. PHP parses the entire file before checking attributes, so syntax errors occur during parsing.

To fix this, use one of these approaches:

Solution 1: Use heredoc syntax (preferred)

-        $code = '<?php $obj = new readonly class {};';
+        $code = <<<'PHP'
+<?php $obj = new readonly class {};
+PHP;

Solution 2: Dynamic string construction

-        $code = '<?php $obj = new readonly class {};';
+        $code = '<?php $obj = new ' . 'readonly' . ' class {};';

Solution 3: Conditional test definition

if (PHP_VERSION_ID >= 80200) {
    // Define the test method here
}

The heredoc approach is cleanest and maintains readability while avoiding parse errors.

🧰 Tools
🪛 GitHub Actions: Tests

[error] 959-959: ParseError: syntax error, unexpected token "readonly"


[error] 982-982: ParseError: syntax error, unexpected token "readonly"

@jackd248 jackd248 merged commit 6950ea2 into main Oct 13, 2025
11 of 12 checks passed
@jackd248 jackd248 deleted the from-composer branch October 13, 2025 11:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants